<?php declare(strict_types=1);
/**
 * @author      xianganyall <xianganyall@gmail.com>
 * @copyright   2023-2025 owner
 **/

namespace Srv\Apps\Apis\Common\Ctl;

use Exception;
use Google\Cloud\Storage\StorageClient;
use Kreait\Firebase\Exception\FirebaseException;
use Kreait\Firebase\Factory;
use Srv\Apps\Apis\Common\DefCtl\DefStorageCategoryCtl;
use Srv\Apps\Proto\Enum\Common\StorageSignUrlEnum;
use Srv\Apps\Proto\VO\Common\StorageGsItemVO;
use Srv\Apps\Proto\VO\Common\StorageGsListVO;
use Srv\Apps\Proto\VO\Common\StorageGsTokenVO;
use Srv\Apps\Proto\VO\Common\StorageStsItemVO;
use Srv\Apps\Proto\VO\Common\StorageStsListVO;
use Srv\Conf\ConfModel\ConfExt;
use Srv\Conf\ConfModel\StorageConf;
use Srv\Libs\Common\CommString;
use Srv\Libs\Common\CommTime;
use Srv\Libs\Common\CommValid;
use Srv\Libs\Frame\ControllerAbstract;
use Srv\Libs\Plugins\Qc\QcCos;
use Srv\Libs\Storage\DataMysql;

final class StorageCtl extends ControllerAbstract
{
    private ?DataMysql $DbAuto                  = null;     // DB-Write
    private array $imageExtList                 = [         // 图片的后缀名[小写]
        'jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'ico', 'svg', 'ttf', 'tif',
    ];
    private array $StorageClientList            = [];       // 存储对象列表
    public const GROUP_NAME_STS_DEF             = 'cos';    // 默认存储的组名
    public const GROUP_NAME_SGS_DEF             = 'res';    // 默认存储的组名
    public const GROUP_NAME_SGS_VOD             = 'vod';    // 视频存储的组名
    public const STORAGE_CATEGORY_STS           = 'STS';    // 存储类别[STS-腾讯云]
    public const STORAGE_CATEGORY_SGS           = 'SGS';    // 存储类别[SGS-谷歌云]

    // 需要用到vod存储的key
    public array $storageVodKeyList             = [
        DefStorageCategoryCtl::KEY_VIDEO_RES
    ];

    /**
     * @return static
     * tag
     */
    final public static function tag():self
    {
        $thisObject                         = self::tagGet();
        if($thisObject instanceof self){
            $thisObject->DbAuto             = self::getDbAuto();
        }
        return $thisObject;
    }

    /**
     * @param string $typeKey
     * @return bool
     * isImage
     */
    public function isStorageVod(string $typeKey):bool
    {
        if(strlen($typeKey) < 1) return false;
        return in_array($typeKey, $this->storageVodKeyList, true);
    }

    /**
     * @param string $url
     * @param string $path
     * @param string $bucketName
     * @param string $appId
     * @return bool
     * formatStorageUrl
     */
    public function formatStorageUrl(string $url, string &$path, string &$bucketName, string &$appId):bool
    {
        if(!preg_match("/^(\S+)-(\S+):(\S+)$/i", $url, $match)) return false;
        $bucketName     = $match[1];
        $appId          = $match[2];
        $path           = $match[3];
        return true;
    }

    /**
     * @param string $url
     * @param bool $isFull
     * @param int $type
     * @param StorageConf|null $StorageConf
     * @param bool $isSigned
     * @param int $ttl
     * @return string
     * getStorageUrlForType
     */
    private function getStorageUrlForType(string $url, bool $isFull, int $type, ?StorageConf &$StorageConf = null, bool $isSigned = false, int $ttl = 0):string
    {
        if(strlen($url) < 1) return '';
        if(CommValid::isHttps($url) || CommValid::isHttp($url)) return $url;
        $path               = '';
        $bucketName         = '';
        $appId              = '';
        if(!$this->formatStorageUrl($url, $path, $bucketName, $appId)) return $url;
        $StorageConfList    = ConfExt::getStorageConfGroup()->getGroupNameList();
        if(count($StorageConfList) < 1) return $url;
        $StorageConf        = null;
        foreach ($StorageConfList as $StorageConf) if(($StorageConf instanceof StorageConf) && $StorageConf->getAppId() === $appId && $bucketName === $StorageConf->getAppName()) break;
        if(!($StorageConf instanceof StorageConf)) return $path;
        if($isSigned){      // 签名访问URL
            $pathSignedUrl  = '';
            switch ($StorageConf->getCategory()){
                case self::STORAGE_CATEGORY_STS:{
                    $pathSignedUrl  = (strpos($path, '?') === false ? '?' : '&').(new QcCos($StorageConf))->genPreSignQuery($path);
                    break;
                }
                case self::STORAGE_CATEGORY_SGS:{
                    $pathSignedUrl  = $this->genSignedUrlForGs($StorageConf, $path, $ttl);
                    if(preg_match('/^(https|http):\/\/[^\/]+\/[^\/]+(\/.*)/i', $pathSignedUrl, $pathMatch)) $pathSignedUrl = $pathMatch[2];
                    break;
                }
            }
            if(strlen($pathSignedUrl) > 0) $path = $pathSignedUrl;
        }
        switch ($type){
            case 1 : {  // image
                return ($isFull ? $StorageConf->getHttpImage() : '').$path;
            }
            case 2 : {  // file
                return ($isFull ? $StorageConf->getHttpFile() : '').$path;
            }
        }
        if(!$isFull) return $path;
        $ext            = strtolower(substr($path, strrpos($path, '.')+1));
        return (in_array($ext, $this->imageExtList, true) ? $StorageConf->getHttpImage() : $StorageConf->getHttpFile()).$path;
    }

    /**
     * @param string $url
     * @param bool $isFull
     * @param StorageConf|null $StorageConf
     * @param bool $isSigned
     * @param int $ttl
     * @return string
     * getStorageUrlImage
     */
    public function getStorageUrlImage(string $url, bool $isFull, ?StorageConf &$StorageConf = null, bool $isSigned = false, int $ttl = 0):string
    {
        return $this->getStorageUrlForType($url, $isFull, 1, $StorageConf, $isSigned, $ttl);
    }

    /**
     * @param string $url
     * @param bool $isFull
     * @param StorageConf|null $StorageConf
     * @param bool $isSigned
     * @param int $ttl
     * @return string
     * getStorageUrlFile
     */
    public function getStorageUrlFile(string $url, bool $isFull, ?StorageConf &$StorageConf = null, bool $isSigned = false, int $ttl = 0):string
    {
        return $this->getStorageUrlForType($url, $isFull, 2, $StorageConf, $isSigned, $ttl);
    }

    /**
     * @param string $url
     * @param bool $isFull
     * @param StorageConf|null $StorageConf
     * @param bool $isSigned
     * @param int $ttl
     * @return string
     * getStorageUrl
     */
    public function getStorageUrl(string $url, bool $isFull, ?StorageConf &$StorageConf = null, bool $isSigned = false, int $ttl = 0):string
    {
        return $this->getStorageUrlForType($url, $isFull, 0, $StorageConf, $isSigned, $ttl);
    }

    /**
     * @param string $url
     * @return string
     * getStorageSqlUrl
     */
    public function getStorageSqlUrl(string $url):string
    {
        if(CommValid::isHttps($url) || CommValid::isHttp($url)){
            $StorageConfList            = ConfExt::getStorageConfGroup()->getGroupNameList();
            if(count($StorageConfList) < 1) return $url;
            foreach($StorageConfList as $StorageConf){
                if(!($StorageConf instanceof StorageConf)) continue;
                $httpImagePrefix        = $StorageConf->getHttpImage();
                $httpFilePrefix         = $StorageConf->getHttpFile();
                $httpImagePrefixLength  = strlen($httpImagePrefix);
                $httpFilePrefixLength   = strlen($httpFilePrefix);
                if($httpImagePrefixLength > 0 && substr($url, 0, $httpImagePrefixLength) === $httpImagePrefix && substr($url, $httpImagePrefixLength, 1) === '/'){
                    $path               = substr($url, $httpImagePrefixLength);
                    return $StorageConf->getAppName().'-'.$StorageConf->getAppId().':'.$path;
                }
                if($httpFilePrefixLength > 0 && substr($url, 0, $httpFilePrefixLength) === $httpFilePrefix && substr($url, $httpFilePrefixLength, 1) === '/'){
                    $path               = substr($url, $httpFilePrefixLength);
                    return $StorageConf->getAppName().'-'.$StorageConf->getAppId().':'.$path;
                }
            }
        }
        return $url;
    }

    /**
     * @param int $userId
     * @param int $dsacId
     * @param string $key
     * @param string $url
     * @param int $firstTime
     * @param int $expireTime
     * @return int
     * addStorageUrlRecord
     */
    public function addStorageUrlRecord(int $userId, int $dsacId, string $key, string $url, int $firstTime, int $expireTime = 0):int
    {
        if($userId < 0 || $dsacId < 1 || strlen($key) < 1 || strlen($url) < 1 || $firstTime < 1) return 0;
        return $this->DbAuto->getDataId('INSERT INTO '.$this->DbAuto->getTableNameAll('storage_url_record').' SET user_id='.$userId.', dsac_id='.$dsacId.', saur_key=\''.$this->DbAuto->getRealEscapeString($key).'\', saur_url=\''.$this->DbAuto->getRealEscapeString($url).'\', saur_create_time='.$firstTime.', saur_expire_time='.$expireTime);
    }

    /**
     * @param int $userId
     * @param int $dsacId
     * @param string $key
     * @param int $createTime
     * @return string
     * getStorageUrlRecord
     */
    public function getStorageUrlRecord(int $userId, int $dsacId, string $key, int &$createTime = 0):string
    {
        if($userId < 0 || $dsacId < 1 || strlen($key) < 1) return '';
        $storageInfo    = $this->DbAuto->getDataOne('SELECT saur_id, saur_url, saur_create_time FROM '.$this->DbAuto->getTableNameAll('storage_url_record').' WHERE user_id='.$userId.' AND dsac_id='.$dsacId.' AND saur_key=\''.$this->DbAuto->getRealEscapeString($key).'\' AND (saur_expire_time=0 OR saur_expire_time >='.CommTime::getTimeStamp().')');
        if(!isset($storageInfo['saur_id'])) return '';
        $createTime     = intval($storageInfo['saur_create_time']??'');
        return strval($storageInfo['saur_url']??'');
    }

    /**
     * @param int $userId
     * @param int $dsacId
     * @param string $key
     * @return bool
     * delStorageUrlRecord
     */
    public function delStorageUrlRecord(int $userId, int $dsacId, string $key):bool
    {
        if($userId < 0 || $dsacId < 1 || strlen($key) < 1) return false;
        return $this->DbAuto->getDataNum('DELETE FROM '.$this->DbAuto->getTableNameAll('storage_url_record').' WHERE user_id='.$userId.' AND saur_key=\''.$this->DbAuto->getRealEscapeString($key).'\'') >= 0;
    }

    /**
     * @param int $userId
     * @param int $dsacId
     * @param array $keyList
     * @return bool
     * batchDelStorageUrlRecord
     */
    public function batchDelStorageUrlRecord(int $userId, int $dsacId, array $keyList):bool
    {
        if($userId < 0 || $dsacId < 1) return false;
        if(count($keyList) < 1) return true;
        $keyList                = array_unique($keyList);
        $keyBatchList           = [];
        foreach($keyList as $key){
            if(!is_string($key) || strlen($key) < 1) continue;
            $keyBatchList[]     = $this->DbAuto->getRealEscapeString($key);
        }
        if(count($keyBatchList) < 1) return false;
        return $this->DbAuto->getDataNum('DELETE FROM '.$this->DbAuto->getTableNameAll('storage_url_record').' WHERE user_id='.$userId.' AND saur_key IN (\''.implode('\',\'', $keyBatchList).'\')') >= 0;
    }

    /**
     * @param int $userId
     * @param int $dsacId
     * @param array $extNameList
     * @param string $groupName
     * @return StorageStsListVO|null
     * getStorageSTS
     */
    public function getStorageSTS(int $userId, int $dsacId, array $extNameList, string $groupName):?StorageStsListVO
    {
        if($userId < 0 || $dsacId < 1 || count($extNameList) < 1) return null;
        $currTime           = CommTime::getTimeStamp();
        $StorageConf        = ConfExt::getStorageConfGroup()->getByGroupName($groupName);
        $expireTimeSet      = 3600*2;
        $expireTime         = $currTime + $expireTimeSet;
        $QcCos              = new QcCos($StorageConf);
        $pathList           = [];
        $pathDataList       = [];
        $typeKey            = DefStorageCategoryCtl::tag()->getKeyById($dsacId);
        foreach($extNameList as $extName){
            $savePath       = '';
            $remotePathPre  = '';
            if(!$this->genStorageFileForStsSavePath($userId, $typeKey, $extName, $StorageConf, $savePath, $remotePathPre)) continue;
            $key            = substr(sha1($remotePathPre), 0, 24);  // 长度不能超过32位
            $pathDataList[] = ['key' => $key, 'saveUrl' => $remotePathPre, 'path' => $savePath];
            $pathList[]     = $savePath;
        }
        $pathListSize       = count($pathList);
        if($pathListSize < 1) return null;
        if($pathListSize === 1){
            $tmpKeyData     = $QcCos->getTempKeys($pathList[0], $expireTimeSet);
        }else{
            $tmpKeyData     = $QcCos->getTempKeysMulti($pathList, $expireTimeSet);
        }
        if(!isset($tmpKeyData['tmpSecretId'])) return null;
        $StorageStsItemVOList           = [];
        foreach($pathDataList as $item){
            $StorageStsItemVO           = new StorageStsItemVO();
            $StorageStsItemVO->setKey($item['key']);
            $StorageStsItemVO->setUrl($this->getStorageUrl($item['saveUrl'], true));
            $StorageStsItemVO->setPath($item['path']);
            $StorageStsItemVO->setAppId($StorageConf->getAppId());
            $StorageStsItemVO->setBucketName($StorageConf->getAppName());
            $StorageStsItemVO->setPoint($StorageConf->getPoint());
            $StorageStsItemVOList[]     = $StorageStsItemVO;
            $this->addStorageUrlRecord($userId, $dsacId, $item['key'], $item['saveUrl'], $currTime, $expireTime);
        }
        if(count($StorageStsItemVOList) < 1) return null;
        $StorageStsListVO   = new StorageStsListVO();
        $StorageStsListVO->setTmpSecretId($tmpKeyData['tmpSecretId']);
        $StorageStsListVO->setTmpSecretKey($tmpKeyData['tmpSecretKey']);
        $StorageStsListVO->setSessionToken($tmpKeyData['sessionToken']);
        $StorageStsListVO->setExpireTime($tmpKeyData['expireTime']);
        $StorageStsListVO->setStartTime($tmpKeyData['startTime']);
        $StorageStsListVO->setList($StorageStsItemVOList);
        return $StorageStsListVO;
    }

    /**
     * @param int $userId
     * @param string $typeKey
     * @param string $extName
     * @param StorageConf $StorageConf
     * @param string $savePath
     * @param string $remotePath
     * @return bool
     * genStorageFileForStsSavePath
     */
    public function genStorageFileForStsSavePath(int $userId, string $typeKey, string $extName, StorageConf $StorageConf, string &$savePath, string &$remotePath):bool
    {
        if($userId < 0 || strlen($typeKey) < 1 || strlen($extName) < 1) return false;
        $pathPrefix         = DefStorageCategoryCtl::tag()->getExtStrByKey($typeKey, DefStorageCategoryCtl::EXT_PATH_PREFIX);
        $currTime           = CommTime::getTimeStamp();
        $userHex            = dechex(intval(($currTime+$userId).'0'));
        $name               = md5(CommString::genRandStr(8).$userHex);
        $savePath           = $pathPrefix.'/'.CommTime::getString('ymd', $currTime).'/'.substr($name, 0, 3).'/'.substr($name, 3).'.'.$extName;
        $remotePath         = $StorageConf->getAppName().'-'.$StorageConf->getAppId().':'.$savePath;
        return true;
    }

    /**
     * @param string $savePath
     * @param StorageConf $StorageConf
     * @param string $remotePath
     * @return bool
     * genStorageFileForStsPath
     */
    public function genStorageFileForStsPath(string $savePath, StorageConf $StorageConf, string &$remotePath):bool
    {
        if(strlen($savePath) < 2 || substr($savePath, 0, 1) !== '/') return false;
        if(strlen($StorageConf->getAppName()) < 1 || strlen($StorageConf->getAppId()) < 1) return false;
        $remotePath         = $StorageConf->getAppName().'-'.$StorageConf->getAppId().':'.$savePath;
        return true;
    }

    /**
     * @param int $userId
     * @param string $typeKey
     * @param string $localPath
     * @param StorageConf $StorageConf
     * @param string $remotePath
     * @return bool
     * 保存腾讯云存储文件
     */
    public function saveStorageFileForSts(int $userId, string $typeKey, string $localPath, StorageConf $StorageConf, string &$remotePath):bool
    {
        if($userId < 0 || strlen($typeKey) < 1 || strlen($localPath) < 1) return false;
        $extName            = strtolower(substr($localPath, strrpos($localPath, '.')+1));
        if(strlen($extName) < 1 || !file_exists($localPath)) return false;
        $savePath           = '';
        $remotePathPre      = '';
        if(!$this->genStorageFileForStsSavePath($userId, $typeKey, $extName, $StorageConf, $savePath, $remotePathPre)) return false;
        $QcCos              = new QcCos($StorageConf);
        if(!$QcCos->uploadFile($savePath, $localPath)) return false;
        $remotePath         = $StorageConf->getAppName().'-'.$StorageConf->getAppId().':'.$savePath;
        return true;
    }

    /**
     * @param string $savePath
     * @param string $localPath
     * @param StorageConf $StorageConf
     * @param string $remotePath
     * @return bool
     * saveStorageFileForStsPath
     */
    public function saveStorageFileForStsPath(string $savePath, string $localPath, StorageConf $StorageConf, string &$remotePath):bool
    {
        if(strlen($savePath) < 2 || strlen($localPath) < 1 || substr($savePath, 0, 1) !== '/') return false;
        if(!file_exists($localPath)) return false;
        $QcCos              = new QcCos($StorageConf);
        if(!$QcCos->uploadFile($savePath, $localPath)) return false;
        $remotePath         = $StorageConf->getAppName().'-'.$StorageConf->getAppId().':'.$savePath;
        return true;
    }

    /**
     * @param int $userId
     * @param int $dsacId
     * @param array $extNameList
     * @param int $signUrlStatus
     * @param string $extDataJson
     * @param string $groupName
     * @return StorageGsListVO|null
     * getStorageGS
     */
    public function getStorageGS(int $userId, int $dsacId, array $extNameList, int $signUrlStatus, string $extDataJson, string $groupName):?StorageGsListVO
    {
        if($userId < 0 || $dsacId < 1 || count($extNameList) < 1) return null;
        unset($extDataJson);
        $currTime           = CommTime::getTimeStamp();
        $StorageConf        = ConfExt::getStorageConfGroup()->getByGroupName($groupName);
        $expireTimeSet      = 3600*2;
        $expireTime         = $currTime + $expireTimeSet;
        $pathDataList       = [];
        $typeKey            = DefStorageCategoryCtl::tag()->getKeyById($dsacId);
        foreach($extNameList as $extName){
            $savePath       = '';
            $remotePathPre  = '';
            if(!$this->genStorageFileForGsSavePath($userId, $typeKey, $extName, $StorageConf, $savePath, $remotePathPre)) continue;
            $key            = substr(sha1($remotePathPre), 0, 24);  // 长度不能超过32位
            $pathDataList[] = ['key' => $key, 'saveUrl' => $remotePathPre, 'path' => $savePath, 'signedUrl' => $signUrlStatus === StorageSignUrlEnum::STORAGE_SIGN_URL_YES ? $this->genPutSignedUrlForGs($StorageConf, $savePath) : ''];
        }
        if(count($pathDataList) < 1) return null;
        $StorageGsItemVOList        = [];
        foreach($pathDataList as $item){
            $StorageGsItemVO        = new StorageGsItemVO();
            $StorageGsItemVO->setKey($item['key']);
            $StorageGsItemVO->setUrl($this->getStorageUrl($item['saveUrl'], true));
            $StorageGsItemVO->setPath($item['path']);
            $StorageGsItemVO->setAppId($StorageConf->getAppId());
            $StorageGsItemVO->setBucketName($StorageConf->getAppName());
            if(isset($item['signedUrl']) && strlen($item['signedUrl']) > 0) $StorageGsItemVO->setSignedUrl($item['signedUrl']);
            $StorageGsItemVOList[]  = $StorageGsItemVO;
            $this->addStorageUrlRecord($userId, $dsacId, $item['key'], $item['saveUrl'], $currTime, $expireTime);
        }
        if(count($StorageGsItemVOList) < 1) return null;
        $StorageGsListVO            = new StorageGsListVO();
        $StorageGsListVO->setList($StorageGsItemVOList);
        // 生成自定义Token
        if(strlen($StorageConf->getFirebaseAdmFile()) > 0){
            try{
                $claims             = [     // 附加认证数据
                    'override'  => false,                               // 允许覆盖[true-允许,false-不允许]
                    'pathList'  => array_column($pathDataList, 'path'), // 注意:斜杠开头,带文件名
                ];
                $customTokenTtl     = $StorageConf->getFirebaseCustomTokenTtl();
                $customToken        = (new Factory())->withServiceAccount($StorageConf->getFirebaseAdmFile())->createAuth()->createCustomToken(strval($userId), $claims, $customTokenTtl)->toString();
                if(strlen($customToken) > 0){
                    $StorageGsTokenVO   = new StorageGsTokenVO();
                    $StorageGsTokenVO->setTtl($customTokenTtl);
                    $StorageGsTokenVO->setToken($customToken);
                    $StorageGsListVO->setTokenInfo($StorageGsTokenVO);
                }
            }catch(FirebaseException|Exception $FirebaseException){
                return null;
            }
        }
        return $StorageGsListVO;
    }

    /**
     * @param int $userId
     * @param string $typeKey
     * @param string $extName
     * @param StorageConf $StorageConf
     * @param string $savePath
     * @param string $remotePath
     * @return bool
     * genStorageFileForGsSavePath
     */
    public function genStorageFileForGsSavePath(int $userId, string $typeKey, string $extName, StorageConf $StorageConf, string &$savePath, string &$remotePath):bool
    {
        if($userId < 0 || strlen($typeKey) < 1 || strlen($extName) < 1) return false;
        $pathPrefix         = DefStorageCategoryCtl::tag()->getExtStrByKey($typeKey, DefStorageCategoryCtl::EXT_PATH_PREFIX);
        $currTime           = CommTime::getTimeStamp();
        $userHex            = dechex(intval(($currTime+$userId).'0'));
        $name               = md5(CommString::genRandStr(8).$userHex);
        $savePath           = $pathPrefix.'/'.CommTime::getString('ymd', $currTime).'/'.substr($name, 0, 3).'/'.substr($name, 3).'.'.$extName;
        $remotePath         = $StorageConf->getAppName().'-'.$StorageConf->getAppId().':'.$savePath;
        return true;
    }

    /**
     * @param string $savePath
     * @param StorageConf $StorageConf
     * @param string $remotePath
     * @return bool
     * genStorageFileForGsPath
     */
    public function genStorageFileForGsPath(string $savePath, StorageConf $StorageConf, string &$remotePath):bool
    {
        if(strlen($savePath) < 2 || substr($savePath, 0, 1) !== '/') return false;
        if(strlen($StorageConf->getAppName()) < 1 || strlen($StorageConf->getAppId()) < 1) return false;
        $remotePath         = $StorageConf->getAppName().'-'.$StorageConf->getAppId().':'.$savePath;
        return true;
    }

    /**
     * @param int $userId
     * @param string $typeKey
     * @param string $localPath
     * @param StorageConf $StorageConf
     * @param string $remotePath
     * @return bool
     * 保存谷歌云存储文件
     */
    public function saveStorageFileForGs(int $userId, string $typeKey, string $localPath, StorageConf $StorageConf, string &$remotePath):bool
    {
        if($userId < 0 || strlen($typeKey) < 1 || strlen($localPath) < 1) return false;
        $extName            = strtolower(substr($localPath, strrpos($localPath, '.')+1));
        if(strlen($extName) < 1 || !file_exists($localPath)) return false;
        $savePath           = '';
        $remotePathPre      = '';
        if(!$this->genStorageFileForGsSavePath($userId, $typeKey, $extName, $StorageConf, $savePath, $remotePathPre)) return false;
        $StorageClient      = new StorageClient(['keyFilePath' => $StorageConf->getSecretKeyFile()]);
        $bucket             = $StorageClient->bucket($StorageConf->getAppName());
        $object             = $bucket->upload(fopen($localPath, 'r'), ['name' => ltrim($savePath, '/')]);
        if($object === null) return false;
        $remotePath         = $StorageConf->getAppName().'-'.$StorageConf->getAppId().':'.$savePath;
        return true;
    }

    /**
     * @param string $savePath
     * @param string $localPath
     * @param StorageConf $StorageConf
     * @param string $remotePath
     * @return bool
     * saveStorageFileForGsPath
     */
    public function saveStorageFileForGsPath(string $savePath, string $localPath, StorageConf $StorageConf, string &$remotePath):bool
    {
        if(strlen($savePath) < 2 || strlen($localPath) < 1 || substr($savePath, 0, 1) !== '/') return false;
        if(!file_exists($localPath)) return false;
        $StorageClient      = new StorageClient(['keyFilePath' => $StorageConf->getSecretKeyFile()]);
        $bucket             = $StorageClient->bucket($StorageConf->getAppName());
        $object             = $bucket->upload(fopen($localPath, 'r'), ['name' => ltrim($savePath, '/')]);
        if($object === null) return false;
        $remotePath         = $StorageConf->getAppName().'-'.$StorageConf->getAppId().':'.$savePath;
        return true;
    }

    /**
     * @param StorageConf $StorageConf
     * @param string $path
     * @param int $ttl
     * @return string
     * genPutSignedUrlForGs
     */
    private function genPutSignedUrlForGs(StorageConf $StorageConf, string $path, int $ttl = 0):string
    {
        try{
            $storageKey     = $StorageConf->getAppName().'-'.$StorageConf->getAppId();
            if($ttl < 1) $ttl = $StorageConf->getFirebaseCustomTokenTtl();
            if(!isset($this->StorageClientList[$storageKey])) $this->StorageClientList[$storageKey] = new StorageClient(['keyFilePath' => $StorageConf->getSecretKeyFile()]);
            return $this->StorageClientList[$storageKey]->bucket($StorageConf->getAppName())->object(ltrim($path, '/'))->signedUrl(CommTime::getTimeStamp() + $ttl, ['method' => 'PUT', 'version' => 'v4']);
        }catch (Exception $Exception){
            return '';
        }
    }

    /**
     * @param StorageConf $StorageConf
     * @param string $path
     * @param int $ttl
     * @return string
     * genSignedUrlForGs
     */
    private function genSignedUrlForGs(StorageConf $StorageConf, string $path, int $ttl = 0):string
    {
        try{
            $storageKey     = $StorageConf->getAppName().'-'.$StorageConf->getAppId();
            if($ttl < 1) $ttl = $StorageConf->getFirebaseCustomTokenTtl();
            if(!isset($this->StorageClientList[$storageKey])) $this->StorageClientList[$storageKey] = new StorageClient(['keyFilePath' => $StorageConf->getSecretKeyFile()]);
            return $this->StorageClientList[$storageKey]->bucket($StorageConf->getAppName())->object(ltrim($path, '/'))->signedUrl(CommTime::getTimeStamp() + $ttl, ['method' => 'GET', 'version' => 'v4']);
        }catch (Exception $Exception){
            return '';
        }
    }

    /**
     * @param int $userId
     * @param int $dsacId
     * @param array $extNameList
     * @return StorageStsListVO|null
     * getStorageSTSAtDir
     */
    public function getStorageSTSAtDir(int $userId, int $dsacId, array $extNameList, string $groupName):?StorageStsListVO
    {
        if($userId < 0 || $dsacId < 1 || count($extNameList) < 1) return null;
        $currTime           = CommTime::getTimeStamp();
        $StorageConf        = ConfExt::getStorageConfGroup()->getByGroupName($groupName);
        $expireTimeSet      = 3600*2;
        $expireTime         = $currTime + $expireTimeSet;
        $QcCos              = new QcCos($StorageConf);
        $pathList           = [];
        $pathDataList       = [];
        $typeKey            = DefStorageCategoryCtl::tag()->getKeyById($dsacId);
        foreach($extNameList as $extName){
            $savePath       = '';
            $remotePathPre  = '';
            if(!$this->genStorageFileForStsSavePath($userId, $typeKey, $extName, $StorageConf, $savePath, $remotePathPre)) continue;
            $key            = substr(sha1($remotePathPre), 0, 24);  // 长度不能超过32位
            $pathDataList[] = ['key' => $key, 'saveUrl' => $remotePathPre, 'path' => $savePath];
            $pathList[]     = $savePath;
        }
        $pathListSize       = count($pathList);
        if($pathListSize < 1) return null;
        if($pathListSize === 1){
            $tmpKeyData     = $QcCos->getTempKeys($pathList[0], $expireTimeSet);
        }else{
            $pathPrefix     = DefStorageCategoryCtl::tag()->getExtStrByKey($typeKey, DefStorageCategoryCtl::EXT_PATH_PREFIX);
            $tmpKeyData     = $QcCos->getTempKeys($pathPrefix.'/*', $expireTimeSet);
        }
        if(!isset($tmpKeyData['tmpSecretId'])) return null;
        $StorageStsItemVOList           = [];
        foreach($pathDataList as $item){
            $StorageStsItemVO           = new StorageStsItemVO();
            $StorageStsItemVO->setKey($item['key']);
            $StorageStsItemVO->setUrl($this->getStorageUrl($item['saveUrl'], true));
            $StorageStsItemVO->setPath($item['path']);
            $StorageStsItemVO->setAppId($StorageConf->getAppId());
            $StorageStsItemVO->setBucketName($StorageConf->getAppName());
            $StorageStsItemVO->setPoint($StorageConf->getPoint());
            $StorageStsItemVOList[]     = $StorageStsItemVO;
            $this->addStorageUrlRecord($userId, $dsacId, $item['key'], $item['saveUrl'], $currTime, $expireTime);
        }
        if(count($StorageStsItemVOList) < 1) return null;
        $StorageStsListVO   = new StorageStsListVO();
        $StorageStsListVO->setTmpSecretId($tmpKeyData['tmpSecretId']);
        $StorageStsListVO->setTmpSecretKey($tmpKeyData['tmpSecretKey']);
        $StorageStsListVO->setSessionToken($tmpKeyData['sessionToken']);
        $StorageStsListVO->setExpireTime($tmpKeyData['expireTime']);
        $StorageStsListVO->setStartTime($tmpKeyData['startTime']);
        $StorageStsListVO->setList($StorageStsItemVOList);
        return $StorageStsListVO;
    }
}